package config

import (
	"fmt"
	"io/ioutil"
	"strings"

	"github.com/golang/protobuf/jsonpb"
	"github.com/golang/protobuf/proto"
	pb "github.com/linkerd/linkerd2/controller/gen/config"
	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
	log "github.com/sirupsen/logrus"
	"sigs.k8s.io/yaml"
)

// Global returns the Global protobuf config from the linkerd-config ConfigMap
func Global(filepath string) (*pb.Global, error) {
	config := &pb.Global{}
	err := unmarshalFile(filepath, config)
	return config, err
}

// Proxy returns the Proxy protobuf config from the linkerd-config ConfigMap
func Proxy(filepath string) (*pb.Proxy, error) {
	config := &pb.Proxy{}
	err := unmarshalFile(filepath, config)
	return config, err
}

// Install returns the Install protobuf config from the linkerd-config ConfigMap
func Install(filepath string) (*pb.Install, error) {
	config := &pb.Install{}
	err := unmarshalFile(filepath, config)
	return config, err
}

// Values returns the Value struct from the linkerd-config ConfigMap
func Values(filepath string) (*l5dcharts.Values, error) {
	values := &l5dcharts.Values{}
	configYaml, err := ioutil.ReadFile(filepath)
	if err != nil {
		return nil, fmt.Errorf("failed to read config file: %s", err)
	}

	log.Debugf("%s config YAML: %s", filepath, configYaml)
	if err = yaml.Unmarshal(configYaml, values); err != nil {
		return nil, fmt.Errorf("failed to unmarshal JSON from: %s: %s", filepath, err)
	}
	return values, err
}

func unmarshalFile(filepath string, msg proto.Message) error {
	configJSON, err := ioutil.ReadFile(filepath)
	if err != nil {
		return fmt.Errorf("failed to read config file: %s", err)
	}

	log.Debugf("%s config JSON: %s", filepath, configJSON)
	if err = unmarshal(string(configJSON), msg); err != nil {
		return fmt.Errorf("failed to unmarshal JSON from: %s: %s", filepath, err)
	}

	return nil
}

func unmarshal(json string, msg proto.Message) error {
	// If a config is missing, then just leave the message as nil and return
	// without an error.
	if json == "" {
		return nil
	}

	// If we're using older code to read a newer config, blowing up during decoding
	// is not helpful. We should detect that through other means.
	u := jsonpb.Unmarshaler{AllowUnknownFields: true}
	return u.Unmarshal(strings.NewReader(json), msg)
}

// FromConfigMap builds a configuration by reading a map with the keys "global"
// and "proxy", each containing JSON values.
func FromConfigMap(configMap map[string]string) (*pb.All, error) {
	c := &pb.All{Global: &pb.Global{}, Proxy: &pb.Proxy{}, Install: &pb.Install{}}

	if err := unmarshal(configMap["global"], c.Global); err != nil {
		return nil, fmt.Errorf("invalid global config: %s", err)
	}

	if err := unmarshal(configMap["proxy"], c.Proxy); err != nil {
		return nil, fmt.Errorf("invalid proxy config: %s", err)
	}

	if err := unmarshal(configMap["install"], c.Install); err != nil {
		return nil, fmt.Errorf("invalid install config: %s", err)
	}

	return c, nil
}

// ToJSON encode the configuration to JSON, i.e. to be stored in a ConfigMap.
func ToJSON(configs *pb.All) (global, proxy, install string, err error) {
	m := jsonpb.Marshaler{EmitDefaults: true}

	global, err = m.MarshalToString(configs.GetGlobal())
	if err != nil {
		return
	}

	proxy, err = m.MarshalToString(configs.GetProxy())
	if err != nil {
		return
	}

	install, err = m.MarshalToString(configs.GetInstall())
	return
}

// ToValues converts configuration into a Values struct, i.e to be consumed by check
// TODO: Remove this once the newer configuration becomes the default i.e 2.10
func ToValues(configs *pb.All) *l5dcharts.Values {

	// convert install flags into values
	values := &l5dcharts.Values{
		Global: &l5dcharts.Global{
			CNIEnabled:              configs.GetGlobal().GetCniEnabled(),
			Namespace:               configs.GetGlobal().GetLinkerdNamespace(),
			IdentityTrustAnchorsPEM: configs.GetGlobal().GetIdentityContext().GetTrustAnchorsPem(),
			IdentityTrustDomain:     configs.GetGlobal().GetIdentityContext().GetTrustDomain(),
			ClusterDomain:           configs.GetGlobal().GetClusterDomain(),
			ClusterNetworks:         configs.GetProxy().GetDestinationGetNetworks(),
			LinkerdVersion:          configs.GetGlobal().GetVersion(),
			Proxy: &l5dcharts.Proxy{
				Image: &l5dcharts.Image{
					Name:       configs.GetProxy().GetProxyImage().GetImageName(),
					PullPolicy: configs.GetProxy().GetProxyImage().GetPullPolicy(),
					Version:    configs.GetProxy().GetProxyVersion(),
				},
				Ports: &l5dcharts.Ports{
					Control:  int32(configs.GetProxy().GetControlPort().GetPort()),
					Inbound:  int32(configs.GetProxy().GetInboundPort().GetPort()),
					Admin:    int32(configs.GetProxy().GetAdminPort().GetPort()),
					Outbound: int32(configs.GetProxy().GetOutboundPort().GetPort()),
				},
				Resources: &l5dcharts.Resources{
					CPU: l5dcharts.Constraints{
						Limit:   configs.GetProxy().GetResource().GetLimitCpu(),
						Request: configs.GetProxy().GetResource().GetRequestCpu(),
					},
					Memory: l5dcharts.Constraints{
						Limit:   configs.GetProxy().GetResource().GetLimitMemory(),
						Request: configs.GetProxy().GetResource().GetRequestMemory(),
					},
				},
				EnableExternalProfiles: !configs.Proxy.GetDisableExternalProfiles(),
				LogFormat:              configs.GetProxy().GetLogFormat(),
				OutboundConnectTimeout: configs.GetProxy().GetOutboundConnectTimeout(),
				InboundConnectTimeout:  configs.GetProxy().GetInboundConnectTimeout(),
			},
			ProxyInit: &l5dcharts.ProxyInit{
				IgnoreInboundPorts:  toString(configs.GetProxy().GetIgnoreInboundPorts()),
				IgnoreOutboundPorts: toString(configs.GetProxy().GetIgnoreOutboundPorts()),
				Image: &l5dcharts.Image{
					Name:       configs.GetProxy().GetProxyInitImage().GetImageName(),
					PullPolicy: configs.GetProxy().GetProxyInitImage().GetPullPolicy(),
					Version:    configs.GetProxy().GetProxyInitImageVersion(),
				},
			},
		},
		Identity: &l5dcharts.Identity{
			Issuer: &l5dcharts.Issuer{
				Scheme: configs.GetGlobal().GetIdentityContext().GetScheme(),
			},
		},
		OmitWebhookSideEffects: configs.GetGlobal().GetOmitWebhookSideEffects(),
		DebugContainer: &l5dcharts.DebugContainer{
			Image: &l5dcharts.Image{
				Name:       configs.GetProxy().GetDebugImage().GetImageName(),
				PullPolicy: configs.GetProxy().GetDebugImage().GetPullPolicy(),
				Version:    configs.GetProxy().GetDebugImageVersion(),
			},
		},
	}

	// for non-primitive types set only if they are not nil
	if configs.GetGlobal().GetIdentityContext().GetIssuanceLifetime() != nil {
		values.Identity.Issuer.IssuanceLifetime = configs.GetGlobal().GetIdentityContext().GetIssuanceLifetime().String()
	}
	if configs.GetGlobal().GetIdentityContext().GetClockSkewAllowance() != nil {
		values.Identity.Issuer.ClockSkewAllowance = configs.GetGlobal().GetIdentityContext().GetClockSkewAllowance().String()
	}

	if configs.GetProxy().GetLogLevel() != nil {
		values.Global.Proxy.LogLevel = configs.GetProxy().GetLogLevel().String()

	}

	// set HA, and Heartbeat flags as health-check needs them for old config installs
	for _, flag := range configs.GetInstall().GetFlags() {
		if flag.GetName() == "ha" && flag.GetValue() == "true" {
			values.Global.HighAvailability = true
		}

		if flag.GetName() == "disable-heartbeat" && flag.GetValue() == "true" {
			values.DisableHeartBeat = true
		}
	}

	return values
}

func toString(portRanges []*pb.PortRange) string {

	var portRangeString string
	if len(portRanges) > 0 {
		for i := 0; i < len(portRanges)-1; i++ {
			portRangeString += portRanges[i].GetPortRange() + ","
		}

		portRangeString += portRanges[len(portRanges)-1].GetPortRange()
	}

	return portRangeString
}
